package com.company.nuwa.redis.support.lock;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.util.Assert;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * <p>redis locker</p >
 *
 * @author Nikola Tesla
 * @version 1.0
 * @date 2021/1/12 14:04
 */
@Slf4j
public class RedisLocker {

    private final RedissonClient redissonClient;

    private final LockKeyBuilder lockKeyBuilder;

    private final LockFailureStrategy lockFailureStrategy;

    public RedisLocker(RedissonClient redissonClient, LockKeyBuilder lockKeyBuilder, LockFailureStrategy lockFailureStrategy) {
        this.redissonClient = redissonClient;
        this.lockKeyBuilder = lockKeyBuilder;
        this.lockFailureStrategy = lockFailureStrategy;
    }

    /**
     * Default lock 1000 millis for lockKey.
     *
     * @param lockKey lock key
     * @param call    Callable object
     * @return callable value
     */
    @SneakyThrows
    public <V> V lock(String lockKey, Callable<V> call) {
        return lock(lockKey, 1000, TimeUnit.MILLISECONDS, call);
    }

    /**
     * Default wait 1000 millis to get lock for lockKey.
     *
     * @param lockKey lock key
     * @param success Callable object
     * @return callable value
     */
    @SneakyThrows
    public <V> V tryLock(String lockKey, Callable<V> success, Callable<V> fail) {
        return tryLock(lockKey, 1000, TimeUnit.MILLISECONDS, success, fail);
    }

    /**
     * @param lockKey  lock key
     * @param waitTime the maximum time to wait for the lock
     * @param unit     time unit
     * @param success  Callable object for locking success
     * @return callable value
     */
    @SneakyThrows
    public <V> V tryLock(String lockKey, int waitTime, TimeUnit unit, Callable<V> success) {
        return tryLock(lockKey, waitTime, unit, success, null);
    }

    /**
     * @param lockKey   lock key
     * @param leaseTime the maximum time to hold the lock after granting it, before automatically releasing it if it
     *                  hasn't already been released by invoking <code>unlock</code>. If leaseTime is -1, hold the lock
     *                  until explicitly unlocked.
     * @param unit      time unit
     * @param call      Callable object
     * @return callable value
     */
    @SneakyThrows
    public <V> V lock(String lockKey, int leaseTime, TimeUnit unit, Callable<V> call) {
        Assert.notNull(call, "Callback object must not be null");

        RLock lock = redissonClient.getLock(lockKeyBuilder.getKeyPrefix() + lockKey);
        lock.lock(leaseTime, unit);

        if (lock.isHeldByCurrentThread()) {
            try {
                return call.call();
            } finally {
                lock.unlock();
            }
        }

        log.warn("current thread {} does not get redis lock for key : {}",
                Thread.currentThread().getName(), lockKey);

        return null;
    }

    /**
     * @param lockKey  lock key
     * @param waitTime the maximum time to wait for the lock
     * @param unit     time unit
     * @param success  Callable object for locking success
     * @param fail     Callable object for getting lock failure
     * @return callable value
     */
    @SneakyThrows
    public <V> V tryLock(String lockKey, int waitTime, TimeUnit unit, Callable<V> success, Callable<V> fail) {
        Assert.notNull(success, "Callback object must not be null");

        RLock lock = redissonClient.getLock(lockKeyBuilder.getKeyPrefix() + lockKey);
        boolean locked = lock.tryLock(waitTime, unit);

        if (locked) {
            try {
                return success.call();
            } finally {
                lock.unlock();
            }
        } else {
            log.warn("current thread {} does not get redis lock for key : {}",
                    Thread.currentThread().getName(), lockKey);
            if (fail != null) {
                log.info("execute failure callback for lock key : {}", lockKey);
                return fail.call();
            } else {
                lockFailureStrategy.onLockFailure(lockKey, waitTime, unit);
            }
        }

        return null;
    }

}
